package com.loocup.filter;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @author loocup
 * @description
 * @since 2019/3/12
 */
@Slf4j
public class LogFilterWithGelf extends OncePerRequestFilter {

//    private RemoteLogger gelfLogger;

    private static final String STREAMING_ATTRIBUTE = LogFilterWithGelf.class.getName() + ".STREAMING";

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return true;
    }

    @Override
    protected void initFilterBean() throws ServletException {
        ApplicationContext ac = (ApplicationContext) getFilterConfig().getServletContext()
                .getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
//        gelfLogger = (RemoteLogger) ac.getBean("gelfLogger");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        if (isApplicationJson(request) || isMultipart(request) || isGet(request)) {
            boolean isFirstRequest = !isAsyncDispatch(request);
            HttpServletRequest requestToUse = request;
            if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
                requestToUse = new ContentCachingRequestWrapper(request);
            }

            HttpServletResponse responseToUse = response;
            if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) {
                responseToUse = new HttpStreamingAwareContentCachingResponseWrapper(response, request);
            }
            log.info("contentType {}", requestToUse.getContentType());
            log.info("method {}", requestToUse.getMethod());

            try {
                filterChain.doFilter(requestToUse, responseToUse);
            } finally {
                try {
                    if (!isAsyncStarted(requestToUse)) {
                        if (!requestURI.contains("/api/v1/log")) {
                            Map<String, String> requestMessageMap = wrapperRequest(requestToUse);
                            String responseMessage = wrapperResponse(responseToUse);
                            log.info("request {}", requestMessageMap);
                            log.info("responseMessage {}", responseMessage);
                            if (log.isInfoEnabled()) {
                                log.info("**请求路径** \n{}\n **请求参数** \n {}\n **返回参数** \n{}\n", requestURI, JSON.toJSONString(requestMessageMap), responseMessage);
                            }
                        }else {
                            updateResponse(responseToUse);
                        }
                    }else {
                        updateResponse(responseToUse);
                    }
                } catch (Exception e) {
                    log.error("log filter generate failed");
                }
            }
        } else {
            if (log.isInfoEnabled()) {
                log.info("{} no log, contentType {}, method {}", requestURI, request.getContentType(), method);
            }
            filterChain.doFilter(request, response);
        }
    }

    /**
     * Get the message to write to the log after the request.
     */
    private Map<String, String> wrapperRequest(HttpServletRequest request) {
        Map<String, String> res = new HashMap<>();
        // 参数
        Enumeration<String> parameterNames = request.getParameterNames();
        if (parameterNames != null) {
            Map<String, String> paramMap = new HashMap<>();
            while (parameterNames.hasMoreElements()) {
                String param = parameterNames.nextElement();
                paramMap.put(param, StringUtils.join(request.getParameterValues(param), ","));
            }
            res.put("params", JSON.toJSONString(paramMap));
        }
        // json
        String payload = getMessagePayload(request);
        if (payload != null) {
            res.put("json", payload);
        }
        return res;
    }

    private String wrapperResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
                WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        Assert.notNull(responseWrapper, "ContentCachingResponseWrapper not found");

        HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse();
        // old version 4.14左右 可用
        byte[] body = responseWrapper.getContentAsByteArray();
        String resp = new String(body, StandardCharsets.UTF_8);
        StreamUtils.copy(body, rawResponse.getOutputStream());
        // since 4.2
//        responseWrapper.copyBodyToResponse();
        return resp;
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
                WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        Assert.notNull(responseWrapper, "ShallowEtagResponseWrapper not found");

        HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse();
        int statusCode = responseWrapper.getStatusCode();
        byte[] body = responseWrapper.getContentAsByteArray();

        if (rawResponse.isCommitted()) {
            if (body.length > 0) {
                StreamUtils.copy(body, rawResponse.getOutputStream());
            }
        }
        else {
            if (logger.isTraceEnabled()) {
                logger.trace("Response with status code [" + statusCode + "] not eligible for ETag");
            }
            if (body.length > 0) {
                rawResponse.setContentLength(body.length);
                StreamUtils.copy(body, rawResponse.getOutputStream());
            }
        }
    }

    private String getMessagePayload(HttpServletRequest request) {
        ContentCachingRequestWrapper wrapper =
                WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                int length = buf.length;
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException ex) {
                    return "[unknown]";
                }
            }
        }
        return null;
    }

    private boolean isBinaryContent(final HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getContentType())) {
            return false;
        }
        return request.getContentType().startsWith("image") || request.getContentType().startsWith("video") || request.getContentType().startsWith("audio");
    }

    private boolean isGet(final HttpServletRequest request) {
        return "GET".equals(request.getMethod());
    }

    private boolean isPost(final HttpServletRequest request) {
        return "POST".equals(request.getMethod());
    }

    private boolean isApplicationJson(final HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getContentType())) {
            return false;
        }
        return request.getContentType().equals("application/json");
    }

    private boolean isMultipart(final HttpServletRequest request) {
        return StringUtils.isNotEmpty(request.getContentType()) && request.getContentType().startsWith("multipart/form-data");
    }

    private static boolean isContentCachingDisabled(HttpServletRequest request) {
        return (request.getAttribute(STREAMING_ATTRIBUTE) != null);
    }

    /**
     * 获取IP 适用聚泛目前场景
     *
     * @param request
     * @return
     * @since 2019年3月14日11:00:44
     */
    public String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (log.isDebugEnabled()) {
            log.debug("getIpAddress(HttpServletRequest) - X-Forwarded-For - String ip=" + ip);
        }

        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
            if (log.isDebugEnabled()) {
                log.debug("getIpAddress(HttpServletRequest) - getRemoteAddr - String ip=" + ip);
            }
        }
        if (ip.length() > 15) {
            String[] ips = ip.split(",");
            for (int index = 0; index < ips.length; index++) {
                String strIp = (String) ips[index];
                if (!("unknown".equalsIgnoreCase(strIp))) {
                    ip = strIp;
                    break;
                }
            }
        }
        return ip;
    }

    private static class HttpStreamingAwareContentCachingResponseWrapper extends ContentCachingResponseWrapper {

        private final HttpServletRequest request;

        HttpStreamingAwareContentCachingResponseWrapper(HttpServletResponse response, HttpServletRequest request) {
            super(response);
            this.request = request;
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            return (useRawResponse() ? getResponse().getOutputStream() : super.getOutputStream());
        }

        @Override
        public PrintWriter getWriter() throws IOException {
            return (useRawResponse() ? getResponse().getWriter() : super.getWriter());
        }

        private boolean useRawResponse() {
            return isContentCachingDisabled(this.request);
        }
    }
}

